﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using Emgu;
using Emgu.CV;
using Emgu.CV.Dnn;
using Emgu.CV.UI;
using System.Drawing;
using Emgu.CV.Structure;
using System.Diagnostics;
using Emgu.CV.Util;
using Emgu.CV.CvEnum;

namespace EmgucvYoloV3
{
    
    /// YOLO
    /// https://pjreddie.com/darknet/yolo/
    /// </summary>
    class Program
    {
        #region const/readonly
        //YOLOv3
        //https://github.com/pjreddie/darknet/blob/master/cfg/yolov3.cfg
        private const string Cfg = "yolov3.cfg";

        //https://pjreddie.com/media/files/yolov3.weights
        private const string Weight = "yolov3.weights";

        //https://github.com/pjreddie/darknet/blob/master/data/coco.names
        private const string Names = "coco.names";

        //file location
        private const string Location = "../../Content/";

        //random assign color to each label
        // private static readonly MCvScalar[] Colors = Enumerable.Repeat(false, 80).Select(x => new MCvScalar()).ToArray();

        //get labels from coco.names
        //private static readonly string[] Labels = File.ReadAllLines(Path.Combine(Location, Names)).ToArray();

        private static readonly string[] Labels = File.ReadAllLines(@"F:/sh/myyunshen/color/myvoc.names").ToArray();

        #endregion

        static void Main(string[] args)
        {
            #region parameter
            var image = Path.Combine(Location, "Shinjuku.jpeg");
            var cfg = Path.Combine(Location, Cfg);
            var model = Path.Combine(Location, Weight);
            const float threshold = 0.5f;       //for confidence 
            const float nmsThreshold = 0.3f;    //threshold for nms
            #endregion


            image = @"F:/sh/myyunshen/color/20190916.16.jpg";
            cfg= @"F:/sh/myyunshen/color/myyolov3-voc_test.cfg";


            model= @"F:/sh/myyunshen/color/myyolov3-voc_last.weights";  
            
            //蜻蜓
            //model = @"F:/AI_Config/data_color/ai-voc_last.weights";



            //get image
            var org = new Mat(image);

            //setting blob, size can be:320/416/608
            //opencv blob setting can check here https://github.com/opencv/opencv/tree/master/samples/dnn#object-detection
            var blob = DnnInvoke.BlobFromImage(org, 1.0 / 255, new Size(416, 416), new MCvScalar(), true, false);

            //load model and config, if you got error: "separator_index < line.size()", check your cfg file, must be something wrong.
            var net = DnnInvoke.ReadNetFromDarknet(cfg, model);
            #region set preferable
            net.SetPreferableBackend(Emgu.CV.Dnn.Backend.Cuda);
            /*
            0:DNN_BACKEND_DEFAULT 
            1:DNN_BACKEND_HALIDE 
            2:DNN_BACKEND_INFERENCE_ENGINE
            3:DNN_BACKEND_OPENCV 
             */
            net.SetPreferableTarget(Target.Cuda);
            /*
            0:DNN_TARGET_CPU 
            1:DNN_TARGET_OPENCL
            2:DNN_TARGET_OPENCL_FP16
            3:DNN_TARGET_MYRIAD 
            4:DNN_TARGET_FPGA 
             */
            #endregion

            //input data
            net.SetInput(blob);

            //get output layer name
            var outNames = net.UnconnectedOutLayersNames;
            //create mats for output layer
            var outs_ = outNames.Select(_ => new Mat());

            VectorOfMat outs = new VectorOfMat();
            foreach (var v in outs_)
            {
                outs.Push(v);
            }

            #region forward model

            Stopwatch sw = new Stopwatch();
            sw.Start();



            net.Forward(outs, outNames);

            sw.Stop();
            Console.WriteLine($"Runtime:{sw.ElapsedMilliseconds} ms");

            //get result from all output
            GetResult(outs, org, threshold, nmsThreshold);


            Emgu.CV.CvInvoke.Imshow("died.tw", org);
            Emgu.CV.CvInvoke.WaitKey();
            #endregion


            #region 2次计算 运算时间
            image = @"F:/sh/myyunshen/color/20190916.2.jpg";

            //get image
             org = new Mat(image);
            //setting blob, size can be:320/416/608
            //opencv blob setting can check here https://github.com/opencv/opencv/tree/master/samples/dnn#object-detection
             blob = DnnInvoke.BlobFromImage(org, 1.0 / 255, new Size(416, 416), new MCvScalar(), true, false);

            //input data
            net.SetInput(blob);          
            sw.Restart();

            net.Forward(outs, outNames);
            sw.Stop();
            Console.WriteLine($"Runtime:{sw.ElapsedMilliseconds} ms");

            //get result from all output
            GetResult(outs, org, threshold, nmsThreshold);


            Emgu.CV.CvInvoke.Imshow("died.tw", org);
            Emgu.CV.CvInvoke.WaitKey();
            #endregion

            #region 2次计算 运算时间
            image = @"F:/sh/myyunshen/color/20190916.21.jpg";

            //get image
            org = new Mat(image);
            //setting blob, size can be:320/416/608
            //opencv blob setting can check here https://github.com/opencv/opencv/tree/master/samples/dnn#object-detection
            blob = DnnInvoke.BlobFromImage(org, 1.0 / 255, new Size(416, 416), new MCvScalar(), true, false);

            //input data
            net.SetInput(blob);
            sw.Restart();

            net.Forward(outs, outNames);
            sw.Stop();
            Console.WriteLine($"Runtime:{sw.ElapsedMilliseconds} ms");

            //get result from all output
            GetResult(outs, org, threshold, nmsThreshold);


            Emgu.CV.CvInvoke.Imshow("died.tw", org);
            Emgu.CV.CvInvoke.WaitKey();
            #endregion


            ////get result from all output
            //GetResult(outs, org, threshold, nmsThreshold);


            //Emgu.CV.CvInvoke.Imshow("died.tw", org);
            //Emgu.CV.CvInvoke.WaitKey();

        }


        static float[] rowData = null;
        /// <summary>
        /// Get result form all output
        /// </summary>
        /// <param name="output"></param>
        /// <param name="image"></param>
        /// <param name="threshold"></param>
        /// <param name="nmsThreshold">threshold for nms</param>
        /// <param name="nms">Enable Non-maximum suppression or not</param>
        private static void GetResult(VectorOfMat output, Mat image, float threshold, float nmsThreshold, bool nms = true)
        {
            //for nms
            var classIds = new List<int>();
            //var confidences = new List<float>();
            var probabilities = new List<float>();


            List<float> confidences = new List<float>();

            List<Rectangle> listRect = new List<Rectangle>();
            

            var w = image.Width;
            var h = image.Height;
            /*
             YOLO3 COCO trainval output
             0 1 : center                    2 3 : w/h
             4 : confidence                  5 ~ 84 : class probability 
            */
            const int prefix = 5;   //skip 0~4

            for (int outIndex = 0; outIndex < output.Size; outIndex++)

            {
                Mat prob = output[outIndex];

                for (var i = 0; i < prob.Rows; i++)
                {
                    if (rowData == null)
                    {
                        rowData = new float[prob.Cols];
                    }
                    //拷贝一行数据
                    prob.Row(i).CopyTo(rowData);

                    //置信度
                    float confidence = rowData[4];

                    int maxId = 0;
                    float maxPro = 0;
                    if (confidence > threshold)
                    {
                        //get classes probability 
                        maxIdPro(rowData, prefix, ref maxId, ref maxPro);

                        if (maxPro > threshold)
                        {
                            //get center and width/height
                            var centerX = rowData[0] * w;
                            var centerY = rowData[1] * h;
                            var width = rowData[2] * w;
                            var height = rowData[3] * h;


                            if (!nms)
                            {
                                // draw result (if don't use NMSBoxes)
                                Draw(image, maxId, confidence, maxPro, centerX, centerY, width, height);
                                continue;
                            }

                            //put data to list for NMSBoxes
                            classIds.Add(maxId);
                            confidences.Add(confidence );
                            probabilities.Add(maxPro);
                            listRect.Add(new Rectangle((int)centerX, (int)centerY,(int) width,(int) height));
                        }

                    }

                }
                
            }

            if (!nms) return;

            VectorOfRect bboxes = new VectorOfRect();
            bboxes.Push(listRect.ToArray());

            VectorOfFloat scores = new VectorOfFloat();

            scores.Push(confidences.ToArray());

            VectorOfInt indices = new VectorOfInt();
            //using non-maximum suppression to reduce overlapping low confidence box
            Emgu.CV.Dnn.DnnInvoke.NMSBoxes(bboxes, scores, threshold, nmsThreshold, indices);

            // Console.WriteLine($"NMSBoxes drop {confidences.Count - indices.Length} overlapping result.");

            foreach (var i in indices.ToArray())
            {
                var box = listRect[i];
                Draw(image, classIds[i], confidences[i], probabilities[i], box.X, box.Y, box.Width, box.Height);
            }

        }


        /// <summary>
        /// 
        /// </summary>
        /// <param name="data"></param>
        /// <param name="startIndex"></param>
        /// <param name="maxId"></param>
        /// <param name="maxPro"></param>
        static void maxIdPro(float[] data, int startIndex, ref int maxId, ref float maxPro)
        {
            maxId = -1;
            maxPro = float.MinValue;
            for (int i = startIndex; i < data.Length; i++)
            {
                if (data[i] > maxPro)
                {
                    maxPro = data[i];
                    maxId = i;
                }
            }
            maxId = maxId - startIndex;
        }

        /// <summary>
        /// Draw result to image
        /// </summary>
        /// <param name="image"></param>
        /// <param name="classes"></param>
        /// <param name="confidence"></param>
        /// <param name="probability"></param>
        /// <param name="centerX"></param>
        /// <param name="centerY"></param>
        /// <param name="width"></param>
        /// <param name="height"></param>
        private static void Draw(Mat image, int classes, float confidence, float probability, double centerX, double centerY, double width, double height)
        {
            //label formating
            var label = $"{Labels[classes]} {probability * 100:0.00}%";
            Console.WriteLine($"confidence {confidence * 100:0.00}% {label}");
            double x1 = (centerX - width / 2) < 0 ? 0 : centerX - width / 2; //avoid left side over edge
                                                                             //draw result


            CvInvoke.Rectangle(image, new Rectangle(new Point((int)x1, (int)(centerY - height / 2)), new Size((int)width, (int)height)), new MCvScalar(0, 255, 0), 2);

            int baseline = 0;
            var textSize = CvInvoke.GetTextSize(label, FontFace.HersheyTriplex, 0.5, 1, ref baseline);
            CvInvoke.Rectangle(image, new Rectangle(new Point((int)x1, (int)(centerY - height / 2 - textSize.Height - baseline)),
                new Size(textSize.Width, textSize.Height + baseline)), new MCvScalar(255, 0, 0), -1);
            // var textColor = CvInvoke.Mean(Colors[classes]).Val0 < 70 ? Scalar.White : Scalar.Black;


            MCvScalar textColor = new MCvScalar(0, 0, 255);
            CvInvoke.PutText(image, label, new Point((int)x1, (int)(centerY - height / 2 - baseline)), FontFace.HersheyTriplex, 0.5, textColor);
        }
    }
}
